
# TODO port completely to Net::HTTP
# TODO logout?

require 'rubygems'
require 'curb' # gem install curb
require 'json' # gem install json
require 'logger'
require 'rexml/document'
include REXML
require 'tempfile'

class NetflixClient
	QUEUE_PAGE					= 'http://www.netflix.com/Queue?inqt=wn&lnkctr=queueTab-ELECTRONIC'
	THUMBS_URL					= 'http://cdn-0.nflximg.com/us/boxshots/small/'

protected
	LOGIN_PAGE					= 'http://www.netflix.com/Login'
	LOGIN_POST_PAGE			= 'https://www.netflix.com/Login'
	QUEUE_POST_PAGE			= 'http://www.netflix.com/Queue?inqt=ELECTRONIC'
	BROWSER_USER_AGENT	= 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)'
	PLAYER_USER_AGENT		= 'WmpHostInternetConnection'
	DOWNLOAD_RETRIES		= 10
	BYTES_PER_10MB			= 10485760
	BYTES_PER_GB				= 1073741824

public
	def initialize(log = RAILS_DEFAULT_LOGGER)
		@log = log
		@curl = nil
		@canceled = false
	end

	def login
		@log.debug('login [')
		if @curl
			@log.debug('using existing session')
			@log.debug('login ]')
			return
		end

		@tmpPath = Tempfile.new('netflux-cookies', RAILS_ROOT + '/tmp').path

		@curl = Curl::Easy.new(nil) {|c|
			c.enable_cookies = true
			c.cookiejar = @tmpPath
			c.follow_location = true
			c.headers['User-Agent'] = BROWSER_USER_AGENT
			#c.verbose = true
		}

		# Get login page
		@curl.url = LOGIN_PAGE
		@curl.perform
		@log.debug('retrieved login page')

		# Post login
		@curl.cookiejar = @tmpPath
		@curl.url = LOGIN_POST_PAGE
		@curl.http_post(
			Curl::PostField.content('nextpage', QUEUE_PAGE),
			Curl::PostField.content('email',	Conf.nfSignIn),
			Curl::PostField.content('movieid', ''),
			Curl::PostField.content('trkid', ''),
			Curl::PostField.content('password1', Conf.nfPassword),
			Curl::PostField.content('RememberMe', 'False'),
			Curl::PostField.content('SubmitButton', 'Click Here to Continue')
		)
		@log.debug('retrieved login post')

		if not @curl.body_str.index('<meta http-equiv="refresh" content="0; URL=http://www.netflix.com/Queue?')
			@curl = nil
		end

		@log.debug('login ]')

		not @curl.nil?
	end

	def get_queue
		@log.debug('get_queue [')

		login

		# Get watch instantly queue page
		@curl.cookiejar = @tmpPath
		@curl.url = QUEUE_PAGE
		@curl.perform

		q = @curl.body_str
		@log.debug('retrieved queue page')

		first = q.index(/<tbody id="dvd-queue"/)

		if not first
			@log.error('Unable to locate DVD queue.')
			@log.debug('get_queue ]')
			return
		end

		last = q.index(%r{</tbody>}m, first)

		q = q[first..last] + '/tbody>'

		# Fix missing img end tags
		q.gsub!(/(<img [^>]+[^\/])>/m, '\1/>')

		# Fix ampersands
		q.gsub!(/&/, '&amp;')

		doc = Document.new('<?xml version="1.0"?>' + q)
		@log.debug('parsed queue XML')

		result = []

		doc.root.elements.each('tr') {|tr|
			titleTag = tr.elements.each('td[@class="tt"]/span/a') {}
			next if titleTag.empty?

			title		= titleTag[0].text
			remote	= titleTag[0].attributes['href']
			link		= tr.elements.each('td[@class="wn"]/span/span/span/a') {}[0].attributes['href']
			movieId = link.match(%r{movieid=(\d+)\D})[1]
			@log.info("title \"#{title}\"")
			@log.debug('URL: ' + link)
 			@log.debug('movie ID: ' + movieId)

			result.push({'title' => title, 'remote' => remote, 'link' => link, 'movieId' => movieId})
		}

		@log.debug('get_queue ]')

		result
	end

	def get_download_url(link, targetBitrate = nil)
		@log.debug('get_download_url [')

		login

		# Get movie download URL
		@curl.cookiejar = @tmpPath
		@curl.url = link
		@curl.perform

		playlist = JSON.parse(@curl.body_str.match(/var WNPlaylistMovies = (.+);$/)[1])
		@log.debug('parsed playlist JSON')

		streams = playlist['movies'].first['streams']

		stream = nil

		if targetBitrate
			lowestDiff = nil

			# Find stream with closest bitrate
			streams.each {|s|
				bitrate = s['bitrate']
				log.debug('testing bitrate: ' + bitrate.to_s)

				diff = (targetBitrate - bitrate).abs

				if diff < lowestDiff or not lowestDiff
					lowestDiff = diff
					stream = s
				end
			}
		else
			stream = streams.last
		end

		@log.debug("selected stream #{stream.inspect}")

		@log.debug('get_download_url ]')

		stream['url']
	end

	def download(url, fileName)
		@log.debug('download [')

		# Initialize return values
		result = {:server => nil, :video => nil}

		require 'net/http'
		require 'uri'

 		# Make sure directory exists
		dirs = []

		dir = File.dirname(fileName)

		while not FileTest.exist?(dir)
			dirs.push(dir)
			dir = File.dirname(dir)
		end

		Dir.mkdir(dirs.pop) until dirs.empty?

		File.open(fileName, 'a') {|file|
			size = File.size(fileName)

			if size == 0
				@log.debug('writing initial byte')
				file << '0'
			end

			# Check disk limit
			diskLimit = Conf.DiskLimit
			if diskLimit and not diskLimit.empty?
				diskLimit = diskLimit.to_f * BYTES_PER_GB

				dirSize = 0

				dir = File.dirname(fileName)

				Dir[File.join(dir, '**', '*')].each {|f| dirSize += File.size(f) if File.file?(f)}
				@log.debug("directory \"#{dir}\" initial size #{dirSize} bytes")

				if dirSize >= diskLimit
					@log.info('Disk limit reached.')
					result[:server] = 'disk limit reached'
					return result
				end
			end

			headers = {
				'User-Agent' => PLAYER_USER_AGENT,
				'Range' => "bytes=#{size + 1}-3999999999/*"
			}

			failures = 0

			begin
				redir = false
				uri = URI.parse(url)
				@log.debug('uri.host = ' + uri.host)
				path = uri.request_uri

				@log.debug('path = ' + path)

				http = Net::HTTP.new(uri.host, uri.port)

				http.request_get(path, headers) {|response|
					if response.is_a?(Net::HTTPSuccess)
						bytesRead = 0

						response.read_body {|s|
							file << s

							if @canceled
								@log.info('Download interrupted.')
								return result
							end

							bytesRead += s.length

							if bytesRead > BYTES_PER_10MB
								bytesRead -= BYTES_PER_10MB
								@log.info('.')
							end

							if dirSize
								dirSize += s.length
								if dirSize >= diskLimit
									@log.info('Disk limit reached.')
									result[:server] = 'disk limit reached'
									return result
								end
							end
						}

						@log.debug("total bytes read = #{byteRead}")

					elsif response.is_a?(Net::HTTPRedirection)
							redir = true
							url = response['location']
							@log.debug("redirecting to #{url}")
					end
				}
			end while redir and (failures += 1) < DOWNLOAD_RETRIES

			if failures == DOWNLOAD_RETRIES
				# TODO error
				@log.error("Download \"#{fileName}\" failed #{DOWNLOAD_RETRIES} times.")
				@log.debug('download ]')
				result[:video] = VideoStatus::ERROR
				return result
			end
		}
		@log.info("Download \"#{fileName}\" done.")

		@log.debug('download ]')

		result[:video] = VideoStatus::DOWNLOADED
		result
	end

	def cancel
		@canceled = true
	end

end
